/*
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections.Generic;

using Gtk;
using Glade;
using Mono.Unix;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Backend.Managers;

namespace DCSharp.Gui
{
	public class DownloadPage : Page
	{
		private DownloadManager downloadManager;

		private MessageArea messageArea;

		private ActionGroup actionGroup;
		private Action startAction;
		private Action stopAction;
		private Action removeAction;
		private Action clearAction;

		private DownloadStore downloadStore;
		private TreePath virtualRoot;

		private DownloadView directoryView;
		private DownloadView downloadView;
		private bool handleSelectionChanged;
		private bool showSources;

		private List<FileDownloader> addQueue;
		private List<FileDownloader> activeDownloads;
		private List<FileDownloader> errorDownloads;

		private uint timeout;
		private uint updateId;

#pragma warning disable 649

		[Widget]
		private HPaned hpaned;

		[Widget]
		private ScrolledWindow scrolledWindow;

		[Widget]
		private ScrolledWindow scrolledWindowTree;

		[Widget]
		private HBox actionBox;

		[Widget]
		private CheckButton directoryTreeCheckButton;

		[Widget]
		private CheckButton sourcesCheckButton;

#pragma warning restore 649

		#region Constructors

		public DownloadPage(ConnectionManager connectionManager,
			DownloadManager downloadManager) : base("DownloadPage.glade")
		{
			if (connectionManager == null)
			{
				throw new ArgumentNullException("connectionManager");
			}
			if (downloadManager == null)
			{
				throw new ArgumentNullException("downloadManager");
			}
			this.downloadManager = downloadManager;

			activeDownloads = new List<FileDownloader>();
			errorDownloads = new List<FileDownloader>();
			addQueue = new List<FileDownloader>();

			handleSelectionChanged = true;

			// User interface
			ActionEntry[] entries = new ActionEntry[] {
				new ActionEntry("Start", Stock.MediaPlay, Catalog.GetString("_Start"),
					null, Catalog.GetString("Starts the selected downloads"),
					OnStartActivated),
				new ActionEntry("Stop", Stock.MediaStop, Catalog.GetString("S_top"),
					null, Catalog.GetString("Stops the selected downloads"), OnStopActivated),
				new ActionEntry("Remove", Stock.Remove, null, null,
					Catalog.GetString("Stops and removes the selected downloads"),
					OnRemoveActivated),
				new ActionEntry("Clear", Stock.Clear, Catalog.GetString("_Clean Up"),
					null, Catalog.GetString("Removes the finished downloads"),
					OnClearActivated),

				new ActionEntry("VisibleColumns", null, Catalog.GetString("_Visible Columns..."), null,
					Catalog.GetString("Edit visible columns"),
					null),
			};

			actionGroup = new ActionGroup("DownloadPageActions");
			actionGroup.Add(entries);

			startAction = actionGroup["Start"];
			stopAction = actionGroup["Stop"];
			removeAction = actionGroup["Remove"];
			clearAction = actionGroup["Clear"];

			// Model
			downloadStore = new DownloadStore();

			// Directory tree
			TreeModelFilter filterModel = new TreeModelFilter(downloadStore, null);
			filterModel.VisibleFunc = DirectoryVisibleFunc;

			TreeModelSort sortModel = new TreeModelSort(filterModel);
			sortModel.SetSortColumnId((int)DownloadStore.Column.Name,
				SortType.Ascending);

			directoryView = new DownloadView(connectionManager, sortModel);
			directoryView.KeyPressEvent += OnDirectoryKeyPressEvent;
			directoryView.PopulatePopup += OnPopulatePopup;
			directoryView.Selection.Mode = SelectionMode.Single;
			directoryView.Selection.Changed += OnDirectorySelectionChanged;
			directoryView.SetSizeRequest(140, 0);
			directoryView.HeadersVisible = false;
			directoryView.Show();

			scrolledWindowTree.Add(directoryView);

			// Hide all but the first column
			for (int i = 1; i < directoryView.Columns.Length; i++)
			{
				TreeViewColumn column = directoryView.Columns[i];
				column.Visible = false;
			}

			// Download view
			downloadView = new DownloadView(connectionManager);
			downloadView.KeyPressEvent += OnKeyPressEvent;
			downloadView.PopulatePopup += OnPopulatePopup;
			downloadView.RowActivated += OnRowActivated;
			downloadView.RowExpanded += OnRowExpanded;
			downloadView.Selection.Changed += OnSelectionChanged;
			downloadView.Show();

			scrolledWindow.Add(downloadView);

			UpdateDownloadViewModel(null);

			// Visible columns
			VisibleColumnsWindow.ConnectToWidget(this, actionGroup["VisibleColumns"],
				downloadView.Columns, downloadView.Columns[0]);

			// Events
			directoryTreeCheckButton.Toggled += OnDirectoryTreeCheckButtonToggled;
			sourcesCheckButton.Toggled += OnSourcesCheckButtonToggled;

			// Initialize
			lock (downloadManager.SyncRoot)
			{
				foreach (Downloader downloader in downloadManager)
				{
					FileDownloader fileDownloader = downloader as FileDownloader;
					if (fileDownloader != null)
					{
						IdleAdd(fileDownloader);
					}
				}
				downloadManager.DownloaderAdded += OnDownloaderAdded;
				downloadManager.DownloaderRemoved += OnDownloaderRemoved;
			}
		}

		#endregion

		#region Properties

		public override string Title
		{
			get { return Catalog.GetString("Downloads"); }
		}

		public override Gdk.Pixbuf Icon
		{
			get { return RenderIcon(Stock.GoDown, IconSize.Menu, null); }
		}

		public override string Tooltip
		{
			get
			{
				if (IsWorking)
				{
					return Catalog.GetString("Loading downloads...");
				}

				int active = 0;
				int remaining = 0;
				lock (downloadManager.SyncRoot)
				{
					foreach (Downloader downloader in downloadManager)
					{
						if (downloader is FileDownloader)
						{
							if (downloader.State == DownloadState.Active)
							{
								active++;
							}
							else
							{
								remaining++;
							}
						}
					}
				}

				string tooltip = String.Format(Catalog.GetPluralString(
					"{0} file downloading", "{0} files downloading", active),
					active);

				tooltip += ". ";
				tooltip += String.Format(Catalog.GetPluralString(
					"{0} file waiting", "{0} files waiting", remaining),
					remaining);

				return String.Format(tooltip, downloadManager.Count);
			}
		}

		public override string Status
		{
			get { return Tooltip; }
		}

		public override ActionGroup Actions
		{
			get { return actionGroup; }
		}

		public int PanePosition
		{
			get { return hpaned.Position; }
			set { hpaned.Position = value; }
		}

		public bool DirectoryTreeVisible
		{
			get { return directoryTreeCheckButton.Active; }
			set { directoryTreeCheckButton.Active = value; }
		}

		public bool SourcesVisible
		{
			get { return sourcesCheckButton.Active; }
			set { sourcesCheckButton.Active = value; }
		}

		public int VisibleColumns
		{
			get { return Util.GetVisibleColumns(downloadView.Columns); }
			set { Util.SetVisibleColumns(downloadView.Columns, value | 1); }
		}

		#endregion

		#region Methods

		protected override void OnDestroyed()
		{
			downloadManager.DownloaderAdded -= OnDownloaderAdded;
			downloadManager.DownloaderRemoved -= OnDownloaderRemoved;

			if (timeout != 0)
			{
				GLib.Source.Remove(timeout);
				timeout = 0;
			}
			if (updateId != 0)
			{
				GLib.Source.Remove(updateId);
				updateId = 0;
			}
			base.OnDestroyed();
		}

		protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
		{
			if (evnt.Key == Gdk.Key.BackSpace)
			{
				TreeIter iter;
				if (scrolledWindowTree.Visible && directoryView.Selection.GetSelected(out iter))
				{
					if (directoryView.Model.IterParent(out iter, iter))
					{
						TreePath path = directoryView.Model.GetPath(iter);

						directoryView.Selection.SelectPath(path);
						directoryView.ScrollToCell(path, null, false, 0f, 0f);
					}
					else
					{
						directoryView.Selection.UnselectAll();
					}
					return true;
				}
			}
			return base.OnKeyPressEvent(evnt);
		}

		private void Add(FileDownloader downloader)
		{
			downloadStore.Add(downloader);

			downloader.StateChanged += OnStateChanged;
			downloader.SourceAdded += OnSourceAdded;
			downloader.SourceRemoved += OnSourceRemoved;

			TreeIter iter = downloadStore.GetIter(downloader);
			TreeIter parent;
			if (downloadStore.IterParent(out parent, iter))
			{
				bool loaded = (bool)downloadStore.GetValue(parent, (int)DownloadStore.Column.Loaded);
				if (loaded)
				{
					// Add the sources
					foreach (SourceInfo source in downloader)
					{
						downloadStore.AppendValues(iter, source, source.User.Nick);
					}
					downloadStore.SetValue(iter, (int)DownloadStore.Column.Loaded, true);
				}
			}

			if (downloader.State == DownloadState.Active ||
				downloader.State == DownloadState.Loading)
			{
				activeDownloads.Add(downloader);
				if (timeout == 0)
				{
					timeout = GLib.Timeout.Add(1000, OnRefreshTimeout);
				}
			}
			else if (downloader.State == DownloadState.Stopped &&
				downloader.Exception != null)
			{
				errorDownloads.Add(downloader);
				UpdateErrorDisplay();
			}
		}

		private void Remove(FileDownloader downloader)
		{
			activeDownloads.Remove(downloader);

			downloader.StateChanged -= OnStateChanged;
			downloader.SourceAdded -= OnSourceAdded;
			downloader.SourceRemoved -= OnSourceRemoved;

			downloadStore.Remove(downloader);
		}

		private void Remove(IList<FileDownloader> downloads)
		{
			// Get the selected path
			TreeIter iter;
			TreePath selectedPath = null;
			if (directoryView.Selection.GetSelected(out iter))
			{
				selectedPath = directoryView.Model.GetPath(iter);
			}

			handleSelectionChanged = false;

			// Remove downloads
			foreach (FileDownloader downloader in downloads)
			{
				Remove(downloader);
			}

			handleSelectionChanged = true;

			// Select the next or parent directory
			if (selectedPath != null &&
				!directoryView.Selection.PathIsSelected(selectedPath))
			{
				SelectPathOrParent(directoryView, selectedPath);
			}

			UpdateSensitivity();
		}

		private void IdleAdd(FileDownloader download)
		{
			lock (addQueue)
			{
				addQueue.Add(download);
				if (addQueue.Count == 1)
				{
					GLib.Idle.Add(AddDownloadsInQueue);
				}
			}
		}

		private bool AddDownloadsInQueue()
		{
			IsWorking = true;
			actionBox.Sensitive = false;

			long ticks = DateTime.Now.Ticks;

			while (addQueue.Count > 0)
			{
				FileDownloader download;
				lock (addQueue)
				{
					if (addQueue.Count == 0)
					{
						break;
					}
					download = addQueue[0];
					addQueue.Remove(download);
				}

				Add(download);

				// Are we blocking the GUI?
				if ((DateTime.Now.Ticks - ticks) >= 500000)
				{
					return true;
				}
			}

			IsWorking = false;
			actionBox.Sensitive = true;
			return false;
		}

		private void UpdateErrorDisplay()
		{
			if (errorDownloads.Count > 0)
			{
				if (messageArea == null)
				{
					messageArea = new MessageArea(
						Catalog.GetString("One or more download errors have occured"),
						Catalog.GetString("You should try and resolve the errors and restart or remove the downloads."),
						MessageType.Error);

					Add(messageArea);
				}
				messageArea.ShowAll();
			}
			else if (messageArea != null)
			{
				messageArea.Hide();
			}
		}

		private void UpdateDownloadViewModel(TreePath virtualRoot)
		{
			this.virtualRoot = virtualRoot;

			SortType order = SortType.Ascending;
			int sortColumnId = (int)DownloadStore.Column.Name;

			TreeModelFilter filterModel;
			TreeModelSort sortModel = (TreeModelSort)downloadView.Model;

			if (sortModel != null)
			{
				sortModel.GetSortColumnId(out sortColumnId, out order);
				filterModel = (TreeModelFilter)sortModel.Model;

				sortModel.Dispose();
				filterModel.Dispose();
			}

			// Create the new models
			filterModel = new TreeModelFilter(downloadStore, virtualRoot);
			filterModel.VisibleFunc = DownloadVisibleFunc;

			sortModel = new TreeModelSort(filterModel);
			sortModel.SetSortFunc((int)DownloadStore.Column.Name, SortFunc);
			sortModel.SetSortColumnId(sortColumnId, order);

			downloadView.Model = sortModel;

			UpdateClearActionSensitivity();
		}

		private void UpdateSensitivity()
		{
			UpdateStateActionSensitivity();
			UpdateClearActionSensitivity();
		}

		private void UpdateStateActionSensitivity()
		{
			bool startSensitive = false;
			bool stopSensitive = false;

			FileDownloader[] downloads = downloadView.GetSelectedDownloads(true);
			removeAction.Sensitive = downloads.Length > 0;

			foreach (FileDownloader downloader in downloads)
			{
				if (!stopSensitive &&
					downloader.State != DownloadState.Stopped &&
					downloader.State != DownloadState.Done)
				{
					stopSensitive = true;
				}
				else if (!startSensitive &&
					downloader.State == DownloadState.Stopped)
				{
					startSensitive = true;
				}
				if (startSensitive & stopSensitive)
				{
					break;
				}
			}
			stopAction.Sensitive = stopSensitive;
			startAction.Sensitive = startSensitive;
		}

		private void UpdateClearActionSensitivity()
		{
			clearAction.Sensitive = false;

			downloadView.Model.Foreach(delegate(TreeModel model,
				TreePath path, TreeIter i)
			{
				FileDownloader downloader = model.GetValue(i,
					(int)DownloadStore.Column.Object) as FileDownloader;

				if (downloader != null && downloader.State == DownloadState.Done)
				{
					clearAction.Sensitive = true;
					return true;
				}
				return false;
			});
		}

		private static void LoadSourcesInDir(TreeStore store, TreeIter dirIter)
		{
			TreeIter iter;
			if (!store.IterChildren(out iter, dirIter))
			{
				return;
			}

			bool loaded = (bool)store.GetValue(dirIter, (int)DownloadStore.Column.Loaded);
			if (loaded)
			{
				return;
			}

			do
			{
				FileDownloader download = store.GetValue(iter,
					(int)DownloadStore.Column.Object) as FileDownloader;
				if (download != null)
				{
					loaded = (bool)store.GetValue(iter, (int)DownloadStore.Column.Loaded);
					if (!loaded)
					{
						// Add the sources
						foreach (SourceInfo source in download)
						{
							store.AppendValues(iter, source, source.User.Nick);
						}
						store.SetValue(iter, (int)DownloadStore.Column.Loaded, true);
					}
				}
			}
			while (store.IterNext(ref iter));

			store.SetValue(dirIter, (int)DownloadStore.Column.Loaded, true);
		}

		private void HandleRemoveRecursive(TreeModel model, params TreeIter[] iters)
		{
			if (iters.Length == 0)
			{
				return;
			}

			List<FileDownloader> toRemove = new List<FileDownloader>();

			Queue<TreeIter> queue = new Queue<TreeIter>();
			foreach (TreeIter i in iters)
			{
				queue.Enqueue(i);
			}

			// Iterate through the iters to find the downloads and sources
			TreeIter iter, childIter, parent;
			do
			{
				iter = queue.Dequeue();
				object o = model.GetValue(iter, (int)DownloadStore.Column.Object);

				FileDownloader downloader = o as FileDownloader;
				SourceInfo source = o as SourceInfo;

				if (downloader != null)
				{
					if (!toRemove.Contains(downloader))
					{
						toRemove.Add(downloader);
					}
				}
				else if (source != null)
				{
					if (model.IterParent(out parent, iter))
					{	
						downloader = (FileDownloader)model.GetValue(parent,
							(int)DownloadStore.Column.Object);

						downloader.RemoveSource(source.User);
					}
				}
				else if (model.IterChildren(out childIter, iter))
				{
					do
					{
						queue.Enqueue(childIter);
					}
					while (model.IterNext(ref childIter));
				}
			}
			while (queue.Count > 0);

			// Remove the downloads from the model
			if (toRemove.Count > 0)
			{
				Remove(toRemove);
			}
			foreach (FileDownloader downloader in toRemove)
			{
				downloader.Stop();
				downloadManager.Remove(downloader);
				try
				{
					downloader.RemoveTempFile();
				}
				catch
				{
					// TODO: Display an error dialog
				}
			}
		}

		private void OnDirectoryRemoveSelected(object obj, EventArgs args)
		{
			TreeIter iter;
			if (directoryView.Selection.GetSelected(out iter))
			{
				TreeModelSort sortModel = (TreeModelSort)directoryView.Model;
				TreeModelFilter filterModel = (TreeModelFilter)sortModel.Model;

				// Get the downloadStore iter
				iter = sortModel.ConvertIterToChildIter(iter);
				iter = filterModel.ConvertIterToChildIter(iter);

				HandleRemoveRecursive(downloadStore, iter);
			}
		}

		private void OnRemoveSelected(object obj, EventArgs args)
		{
			HandleRemoveRecursive(downloadView.Model, downloadView.GetSelectedIters());
		}

		private void OnDirectoryKeyPressEvent(object obj, KeyPressEventArgs args)
		{
			if (args.Event.Key == Gdk.Key.Delete)
			{
				OnDirectoryRemoveSelected(obj, EventArgs.Empty);
			}
		}

		private void OnDirectorySelectionChanged(object obj, EventArgs args)
		{
			if (!handleSelectionChanged)
			{
				return;
			}

			TreePath virtualRoot = null;

			TreeIter iter;
			if (directoryView.Selection.GetSelected(out iter))
			{
				TreeModelSort sortModel = (TreeModelSort)directoryView.Model;
				TreeModelFilter filterModel = (TreeModelFilter)sortModel.Model;

				// Get the downloadStore iter
				iter = sortModel.ConvertIterToChildIter(iter);
				iter = filterModel.ConvertIterToChildIter(iter);

				LoadSourcesInDir(downloadStore, iter);

				virtualRoot = downloadStore.GetPath(iter);
			}

			UpdateDownloadViewModel(virtualRoot);
		}

		private void OnKeyPressEvent(object obj, KeyPressEventArgs args)
		{
			if (args.Event.Key == Gdk.Key.Delete)
			{
				OnRemoveSelected(obj, EventArgs.Empty);
			}
		}

		private void OnPopulatePopup(object obj, PopulatePopupArgs args)
		{
			Menu menu = args.Menu;

			MenuItem separator = new SeparatorMenuItem();
			separator.Show();
			menu.Add(separator);

			// Remove menu item
			ImageMenuItem item = new ImageMenuItem(Stock.Remove, null);
			if (obj == downloadView)
			{
				item.Activated += OnRemoveSelected;
			}
			else
			{
				item.Activated += OnDirectoryRemoveSelected;
			}
			item.Show();
			menu.Add(item);
		}

		private void OnRowActivated(object obj, RowActivatedArgs args)
		{
			TreeModelSort sortModel = (TreeModelSort)downloadView.Model;
			TreeModelFilter filterModel = (TreeModelFilter)sortModel.Model;

			if (!scrolledWindowTree.Visible)
			{
				return;
			}

			// Get the path for the downloadStore. The path is null if the
			// virtual root has changed.
			TreePath path = sortModel.ConvertPathToChildPath(args.Path);
			if (path != null)
			{
				path = filterModel.ConvertPathToChildPath(path);

				sortModel = (TreeModelSort)directoryView.Model;
				filterModel = (TreeModelFilter)sortModel.Model;

				// Get the path for directoryView model. The path is null if
				// it's not a directory.
				path = filterModel.ConvertChildPathToPath(path);
				if (path != null)
				{
					path = sortModel.ConvertChildPathToPath(path);

					directoryView.ExpandToPath(path);
					directoryView.Selection.SelectPath(path);
					directoryView.ScrollToCell(path, null, false, 0f, 0f);
				}
			}
		}

		private void OnRowExpanded(object obj, RowExpandedArgs args)
		{
			TreeModelSort sortModel = (TreeModelSort)downloadView.Model;
			TreeModelFilter filterModel = (TreeModelFilter)sortModel.Model;

			TreePath path = sortModel.ConvertPathToChildPath(args.Path);
			path = filterModel.ConvertPathToChildPath(path);

			TreeIter iter;
			if (downloadStore.GetIter(out iter, path))
			{
				LoadSourcesInDir(downloadStore, iter);
			}
		}

		private void OnSelectionChanged(object obj, EventArgs args)
		{
			if (!handleSelectionChanged)
			{
				return;
			}
			UpdateStateActionSensitivity();
		}

		private void OnDirectoryTreeCheckButtonToggled(object obj, EventArgs args)
		{
			bool visible = directoryTreeCheckButton.Active;

			scrolledWindowTree.Visible = visible;
			if (!visible)
			{
				directoryView.Selection.UnselectAll();
			}
		}

		private void OnSourcesCheckButtonToggled(object obj, EventArgs args)
		{
			TreeModelSort sortModel = (TreeModelSort)downloadView.Model;
			TreeModelFilter filterModel = (TreeModelFilter)sortModel.Model;

			showSources = sourcesCheckButton.Active;
			filterModel.Refilter();
		}

		private void OnStartActivated(object obj, EventArgs args)
		{
			foreach (FileDownloader downloader in downloadView.GetSelectedDownloads(true))
			{
				downloader.Start();
			}
		}

		private void OnStopActivated(object obj, EventArgs args)
		{
			foreach (FileDownloader downloader in downloadView.GetSelectedDownloads(true))
			{
				downloader.Stop();
			}
		}

		private void OnRemoveActivated(object obj, EventArgs args)
		{
			OnRemoveSelected(obj, args);
		}

		private void OnClearActivated(object obj, EventArgs args)
		{
			List<FileDownloader> finished = new List<FileDownloader>();

			downloadView.Model.Foreach(delegate(TreeModel model,
				TreePath path, TreeIter i)
			{
				FileDownloader downloader = model.GetValue(i,
					(int)DownloadStore.Column.Object) as FileDownloader;

				if (downloader != null && downloader.State == DownloadState.Done)
				{
					finished.Add(downloader);
				}
				return false;
			});
			Remove(finished);
		}

		private static void SelectPathOrParent(TreeView treeView, TreePath path)
		{
			do
			{
				TreeIter iter;
				if (treeView.Model.GetIter(out iter, path))
				{
					treeView.Selection.SelectPath(path);
					return;
				}
			}
			while (path.Up() && path.Depth > 0);

			treeView.Selection.SelectPath(TreePath.NewFirst());
		}

		private void OnDownloaderAdded(object obj, DownloadEventArgs args)
		{
			FileDownloader downloader = args.Downloader as FileDownloader;
			if (downloader != null)
			{
				IdleAdd(downloader);
			}
		}

		private void OnDownloaderRemoved(object obj, DownloadEventArgs args)
		{
			if (updateId != 0 || !(args.Downloader is FileDownloader))
			{
				return;
			}
			updateId = GLib.Idle.Add(delegate
			{
				// Were any downloads with errors removed?
				foreach (FileDownloader downloader in errorDownloads.ToArray())
				{
					if (!downloadManager.Contains(downloader))
					{
						errorDownloads.Remove(downloader);
					}
				}
				UpdateErrorDisplay();

				// A redraw is enough since we use CellDataFuncs to render the state.
				QueueDraw();

				OnPageChanged();
				UpdateSensitivity();

				updateId = 0;
				return false;
			});
		}

		private void OnStateChanged(object obj, EventArgs args)
		{
			FileDownloader download = (FileDownloader)obj;
			Application.Invoke(delegate
			{
				QueueDraw();

				downloadStore.UpdateDownload(download);
				UpdateDirectories();

				if (errorDownloads.Contains(download))
				{
					errorDownloads.Remove(download);
					UpdateErrorDisplay();
				}

				if (download.State == DownloadState.Active ||
					download.State == DownloadState.Loading)
				{
					if (!activeDownloads.Contains(download))
					{
						activeDownloads.Add(download);
						if (timeout == 0)
						{
							timeout = GLib.Timeout.Add(1000, OnRefreshTimeout);
						}
					}
				}
				else
				{
					if (download.State == DownloadState.Stopped &&
						download.Exception != null)
					{
						errorDownloads.Add(download);
						UpdateErrorDisplay();
					}
					activeDownloads.Remove(download);
				}
				UpdateStateActionSensitivity();
			});
		}

		private void OnSourceAdded(object obj, SourceEventArgs args)
		{
			FileDownloader download = (FileDownloader)obj;
			SourceInfo source = args.Source;

			Application.Invoke(delegate
			{
				if (!downloadStore.Contains(download))
				{
					return;
				}

				TreeIter iter = downloadStore.GetIter(download);

				bool loaded = (bool)downloadStore.GetValue(iter, (int)DownloadStore.Column.Loaded);
				if (loaded)
				{
					// Add the source
					downloadStore.AppendValues(iter, source, source.User.Nick);
					downloadStore.UpdateDownload(download);

					OnPageChanged();
				}
			});
		}

		private void OnSourceRemoved(object obj, SourceEventArgs args)
		{
			FileDownloader download = (FileDownloader)obj;
			SourceInfo source = args.Source;

			Application.Invoke(delegate
			{
				// The download might have been removed since this code is
				// run in an idle handler. Eg. when selecting a download and
				// its sources and pressing delete.
				if (!downloadStore.Contains(download))
				{
					return;
				}

				TreeIter iter = downloadStore.GetIter(download);
				TreeIter sourceIter;

				// Ignore selection changes when removing a source. 
				handleSelectionChanged = false;

				// Find and remove the source
				if (downloadStore.IterChildren(out sourceIter, iter))
				{
					do
					{
						SourceInfo tsource = downloadStore.GetValue(
							sourceIter, (int)DownloadStore.Column.Object) as SourceInfo;
						if (tsource == source)
						{
							downloadStore.Remove(ref sourceIter);
							break;
						}
					}
					while (downloadStore.IterNext(ref sourceIter)); 
				}

				handleSelectionChanged = true;

				downloadStore.UpdateDownload(download);
				OnPageChanged();
			});
		}

		private bool DirectoryVisibleFunc(TreeModel model, TreeIter iter)
		{
			// NOTE: This does NOT work. TreeModelFilter does not seem to like
			// hidden parents (the downloads) and visible children (the sources).
			//
			//FileDownloader download = model.GetValue(iter,
			//	(int)DownloadStore.Column.Object) as FileDownloader;
			//
			//return download == null;

			// obj is null only when the row is a directory
			object obj = model.GetValue(iter, (int)DownloadStore.Column.Object);
			return obj == null;
		}

		private bool DownloadVisibleFunc(TreeModel model, TreeIter iter)
		{
			SourceInfo source = model.GetValue(iter,
				(int)DownloadStore.Column.Object) as SourceInfo;

			if (source != null)
			{
				return showSources;
			}
			return true;
		}

		private int SortFunc(TreeModel model, TreeIter tia, TreeIter tib)
		{
			object obja = model.GetValue(tia, (int)DownloadStore.Column.Object);
			object objb = model.GetValue(tib, (int)DownloadStore.Column.Object);

			if (obja == null && objb is FileDownloader)
			{
				return -1;
			}
			if (obja is FileDownloader && objb == null)
			{
				return 1;
			}

			string namea = model.GetValue(tia, (int)DownloadStore.Column.Name) as string;
			string nameb = model.GetValue(tib, (int)DownloadStore.Column.Name) as string;

			return String.Compare(namea, nameb);
		}

		#region DownloadStore Updating

		private bool OnRefreshTimeout()
		{
			if (activeDownloads.Count > 0)
			{
				Update();
				return true;
			}
			timeout = 0;
			return false;
		}

		private void Update()
		{
			downloadView.QueueDraw();

			foreach (FileDownloader download in activeDownloads)
			{
				// Only update the downloads below the root
				if (virtualRoot != null)
				{
					TreeIter iter = downloadStore.GetIter(download);
					TreePath path = downloadStore.GetPath(iter);

					if (path.IsDescendant(virtualRoot))
					{
						downloadStore.UpdateDownload(download);
					}
				}
				else
				{
					downloadStore.UpdateDownload(download);
				}
			}

			// Update all the directories
			UpdateDirectories();
		}

		private void UpdateDirectories()
		{
			TreeIter iter;
			if (downloadStore.GetIterFirst(out iter))
			{
				do
				{
					UpdateDirectory(iter);
				}
				while (downloadStore.IterNext(ref iter));
			}
		}

		private void UpdateDirectory(TreeIter iter)
		{
			TreeIter childIter;
			if (downloadStore.IterChildren(out childIter, iter))
			{
				do
				{
					UpdateDirectory(childIter);
				}
				while (downloadStore.IterNext(ref childIter));

				downloadStore.UpdateDirectory(iter);
			}
		}

		#endregion

		#endregion
	}
}
